我們在前幾天已經認識了useReducer和useContext的用法,今天來點進階的內容,把useReducer和useContext結合在一起使用看看。這兩個hook一起使用可以解決「當元件拆小後,需要往多層傳遞state的狀況」和「複雜操作不好管理」的問題,今天一樣透過實際的例子來了解要怎麼將他們結合來使用。
為了讓多個頁面可以共用相同的元件,我們可能會把元件越拆越小,但是主要的邏輯或從api來的資料只會放在最上面的父層,那state或是有關state的操作就必須透過props層層往下傳遞到要使用的元件。這麼做沒有不行,的確也是一個方式,但是如果需要調整邏輯或做拆分的動作,在這樣的使用方式下,可能就比較不好進行。
這裡直接來看一個類似這樣的情境。有一個頁面被拆分成三層,主要資料和state操作的函式都在父層。
// 這是父層
import { useState } from 'react';
import List from './List';
function App() {
// 主要使用的state
const [ listData, setListData ] = useState([
{
id: 1,
title: 'item 1',
img: 'https://picsum.photos/500/400?random=2',
isFavorite: false,
},
{
id: 2,
title: 'item 2',
img: 'https://picsum.photos/500/400?random=22',
isFavorite: false,
},
{
id: 3,
title: 'item 3',
img: 'https://picsum.photos/500/400?random=41',
isFavorite: false,
},
{
id: 4,
title: 'item 4',
img: 'https://picsum.photos/500/400?random=15',
isFavorite: false,
},
{
id: 5,
title: 'item 5',
img: 'https://picsum.photos/500/400?random=77',
isFavorite: false,
}
]);
// 操作state的函式(新增最愛的item)
const addFavoriteItem = (id) => {
const updatedListData = listData.map((item) => {
if (item.id === id) {
return {
...item,
isFavorite: !item.isFavorite
}
}
return item;
})
setListData(updatedListData)
};
// 操作state的函式(移除item)
const deleteItem = (id) => {
const updatedListData = listData.filter((item) => item.id !== id)
setListData(updatedListData)
};
return (
<div className="App">
// 需要個別把state和兩個針對state操作的動作往下傳
<List listData={listData} addFavoriteItem={addFavoriteItem} deleteItem={deleteItem} />
</div>
);
}
再來是父層裡面的List元件
// 父層底下的List子元件
import ListItem from "./ListItem";
export default function List({listData, addFavoriteItem, deleteItem}) {
return (
<div className="list-container">
{
listData.map((item) => (
<ListItem key={item.id} item={item} addFavoriteItem={addFavoriteItem} deleteItem={deleteItem} />
))
}
</div>
)
}
接著是List裡面的Item元件
// List元件底下的Item子元件
import Button from './Button';
export default function ListItem({item, addFavoriteItem, deleteItem}) {
const handleAddFavoriteItem = () => {
addFavoriteItem(item.id)
};
const handleDeleteItem = () => {
deleteItem(item.id)
};
return (
<div className={`list-item-container ${ item.isFavorite && 'favorite' }`} >
<h2>{item.title}</h2>
<img src={item.img} alt="" />
<Button itemId={item.id} onClick={handleAddFavoriteItem}>Add favorite</Button>
<Button itemId={item.id} onClick={handleDeleteItem}>Delete item</Button>
</div>
)
}
往下還有一個Item元件內的Button元件
export default function Button({onClick, children}) {
return (
<button onClick={onClick}>{children}</button>
)
}
以上的這些程式碼就完成了可以選取成我的最愛,也可以將item刪除的功能。
在這個小範例中,會遇到兩個問題,分別是「需要往下傳的props很多」,以及「需要往下傳的層數很多」。
這還只是一個不算太複雜的結構,但是因為當前的元件切分結構,如果要處理點擊按鈕的動作,就需要把用來處理點擊動作所延伸來更新state的函式往下傳兩層。當日後這樣的結構越來越龐大,功能越來越複雜,也就會讓把多個state和函式往下傳多層的這個做法,變得很不方便。
現在還只有兩個操作state的動作,要把它們往下傳遞,就已經要多寫很多props了,如果今天還有更多操作動作的話,需要往下傳的函式就會變得更多。這時候其實就可以考慮把操作state的動作都統一管理,統一往下傳遞,減少需要往下傳遞的props數量,讓整體程式碼變得更簡潔。
父層App
這裡改成使用useReducer管理state,當要傳遞操作state的動作下去時,就可以只傳遞透過useReducer回傳的dispatch。
import { useReducer } from 'react';
import List from './List';
const initialState = [
{
id: 1,
title: 'item 1',
img: 'https://picsum.photos/500/400?random=2',
isFavorite: false,
},
// ... 略
];
// 改成把state和操作state的動作都統一用reducer管理在一起
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_FAVORITE_ITEM':
return state.map(item =>
item.id === action.payload
? { ...item, isFavorite: !item.isFavorite }
: item
);
case 'DELETE_ITEM':
return state.filter(item =>
item.id !== action.payload,
);
default:
return state;
}
};
function App() {
// 使用useReducer取代useState
const [listData, dispatch] = useReducer(reducer, initialState);
return (
<div className="App">
// 改成把dispatch這個把所有state操作動作都統整起來的函式往下傳
<List listData={listData} dispatch={dispatch} />
</div>
);
}
export default App;
再來是父層裡面的List子元件
繼續把dispatch往下傳
import ListItem from "./ListItem";
export default function List({listData, dispatch}) {
return (
<div className="list-container">
{
listData.map((item) => (
<ListItem key={item.id} item={item} dispatch={dispatch} />
))
}
</div>
)
}
接著是List裡面的Item元件
這裡是真正觸發state操作的地方,一樣變成統一使用dispatch,只要用action type告訴dispatch要進行哪個動作就好。這樣調整之後,針對state的操作就變得更一目瞭然。
export default function ListItem({item, dispatch}) {
const handleAddFavoriteItem = () => {
dispatch({ type: 'ADD_FAVORITE_ITEM', payload: item.id })
};
const handleDeleteItem = () => {
dispatch({ type: 'DELETE_ITEM', payload: item.id })
};
return (
<div className={`list-item-container ${ item.isFavorite && 'favorite' }`} >
<h2>{item.title}</h2>
<img src={item.img} alt="" />
<Button itemId={item.id} onClick={handleAddFavoriteItem}>Add favorite</Button>
<Button itemId={item.id} onClick={handleDeleteItem}>Delete item</Button>
</div>
)
}
經過前面的調整後,雖然減少一個需要往子層傳的props了,但是還是沒改善需要把state層層傳遞的部分。沒錯!這時候useContext就可以派上用場了。
創建一個名為ListDataContext的檔案,裡面會有兩個context,分別是item資料的context
和改動state的dispatch context
。
import { createContext } from 'react';
export const ListDataContext = createContext(null);
export const ListDataDispatchContext = createContext(null);
把前面創建好的context import進父層,並且拿context的provider包在最外層,並用value把要傳下去的listData和dispatch帶上。
因為需要把兩個context都帶上,所以會在使用的父層包兩個provider
import { ListDataContext, ListDataDispatchContext } from './ListDataContext';
// 中間略
function App() {
const [listData, listDataDispatch] = useReducer(reducer, initialState);
return (
<div className="App">
// 把useReducer的state和dispatch透過value帶上
<ListDataContext.Provider value={listData}>
<ListDataDispatchContext.Provider value={listDataDispatch}>
<List />
</ListDataDispatchContext.Provider>
</ListDataContext.Provider>
</div>
);
}
接下來就可以透過useContext,取得當前元件要使用的listData或dispatch,並且可以把原本用props傳遞的部分拿掉。
import ListItem from "./ListItem";
import { useContext } from "react";
import { ListDataContext } from "./ListDataContext";
export default function List() {
const listData = useContext(ListDataContext);
return (
<div className="list-container">
{
listData.map((item) => (
<ListItem key={item.id} item={item} />
))
}
</div>
)
}
import Button from './Button';
import { useContext } from 'react';
import { ListDataDispatchContext } from './ListDataContext';
export default function ListItem({item}) {
const dispatch = useContext(ListDataDispatchContext);
const handleAddFavoriteItem = () => {
dispatch({ type: 'ADD_FAVORITE_ITEM', payload: item.id })
};
const handleDeleteItem = () => {
dispatch({ type: 'DELETE_ITEM', payload: item.id })
};
return (
<div className={`list-item-container ${ item.isFavorite && 'favorite' }`} >
<h2>{item.title}</h2>
<img src={item.img} alt="" />
<Button itemId={item.id} onClick={handleAddFavoriteItem}>Add favorite</Button>
<Button itemId={item.id} onClick={handleDeleteItem}>Delete item</Button>
</div>
)
}
現在寫useReducer和useContext的方式分開的,如果想要讓程式碼更好維護,也可以進一步把useReducer的部分和useContext的部分歸類在同一個檔案中。
比較程式碼調整前和調整後,就可以發現調整後的程式碼可以擺脫層層傳遞資料的困擾,也能讓相關的資料都被放在同一處管理,變得更好維護。今天的內容雖然偏實作,但從實作的過程,也能對useReducer和useContext增加更多的熟悉度。到目前為止我們已經從渲染機制,到實作中常會用到的一些hooks來從Vue學React,接下來會進一步認識Vue和React本身以外的全域狀態管理Libary。
Scaling Up with Reducer and Context